/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package fr.inria.openmath.omapi.implementation;

/**
 *
 * @author Mr.Sam
 */
import java.io.*;
import java.lang.*;
import java.math.BigInteger;
import fr.inria.openmath.omapi.*;

public class BinaryParser extends ParserAdapter implements Parser, Locator {

    boolean initDone = false;

    DataInputStream source;

    ParserHandler parserHandler;

    ParserErrorHandler parserErrorHandler;

    // input buffer for token aggregation
    final static int BUFFER_MAX_RESTART_CAPACITY = 2 ^ 12;

    final static int BUFFER_START_CAPACITY = 2 ^ 10;

    final static int BUFFER_GROW_MULT = 1;

    final static int BUFFER_GROW_MAX_INCREMENT = 2 ^ 16;

    Table stringTable;

    Table symbolTable;

    Table variableTable;

    byte[] buffer;

    int bufferCapacity;

    int bufferLen;

    // int parsing
    final static int BIG_INT_BASE_10 = 1;

    final static int BIG_INT_BASE_16 = 2;

    int intSign;

    int intFormat;

    // for locator
    int pos;

    // optimized-one-char-look-ahead for input stream
    final static int HEAD_EMPTY = 0;

    final static int HEAD_READY = 1;

    final static int HEAD_UNGET = 2;

    byte sourceHead;

    int sourceHeadState;

    // store objects for later sharing references
    private class Table {

        int id;

        Object share[];

        public Table () {
            share = new Object[256];
        }

        public void clear() {
            int i;
            for (i = 0; i < id; i++) {
                share[i] = null;
            }
            id = 0;
        }

        public void add(Object o) {
            if (id < 256) {
                share[id] = o;
                id++;
            }
        }

        public String toString() {
            int i;
            String s = "";
            for (i = 0; i < id; i++) {
                s += i + " -> " + share[i] + "\n";
            }
            return s;
        }

    }


    public BinaryParser (ParserHandler parserHandler,
            ParserErrorHandler parserErrorHandler) {
        this.parserHandler = parserHandler;
        this.parserErrorHandler = parserErrorHandler;
    }

    public BinaryParser (ParserHandler parserHandler) {
        this(parserHandler, new ParserErrorAdapter());
    }

    public BinaryParser () {
        this(new ParserAdapter(), new ParserErrorAdapter());
    }

    private byte lookByte() throws IOException {
        if (sourceHeadState == HEAD_EMPTY) {
            sourceHead = source.readByte();
            pos++;
            sourceHeadState = HEAD_READY;
        } else if (sourceHeadState == HEAD_UNGET) {
            sourceHeadState = HEAD_READY;
        }
        return sourceHead;
    }

    private byte getByte() throws IOException {
        byte c;

        if (sourceHeadState == HEAD_EMPTY) {
            c = source.readByte();
            pos++;
            return c;
        } else {
            sourceHeadState = HEAD_EMPTY;
            return sourceHead;
        }
    }

    private void ungetByte(byte c) throws IOException {
        if (sourceHeadState == HEAD_EMPTY) {
            sourceHead = c;
            sourceHeadState = HEAD_UNGET;
        } else {
            throw new IOException("can't unget twice...");
        }
    }


    private byte[] getBytes(int len) throws OMException, IOException {
        if (bufferCapacity < len) {
            expandBuffer(len);
        }
        source.readFully(buffer, 0, len);
        pos += len;
        return buffer;
    }

    private String getString(int len) throws OMException, IOException {
        return new String(getBytes(len), 0, len, "ISO8859_1");
    }

    private void resetBuffer() {
        bufferLen = 0;
        if (bufferCapacity > BUFFER_MAX_RESTART_CAPACITY) {
            buffer = new byte[BUFFER_MAX_RESTART_CAPACITY];
            bufferCapacity = BUFFER_MAX_RESTART_CAPACITY;
        }
    }

    private void resizeBuffer() {
        byte[] newBuffer;
        if (bufferCapacity != 0) {
            bufferCapacity += Math.min(BUFFER_GROW_MAX_INCREMENT,
                    bufferCapacity * BUFFER_GROW_MULT);
            newBuffer = new byte[bufferCapacity];
            System.arraycopy(buffer, 0, newBuffer, 0, bufferLen);
            buffer = newBuffer;
        } else {
            bufferCapacity = BUFFER_START_CAPACITY;
            buffer = new byte[bufferCapacity];
        }
    }

    private void expandBuffer(int len) {
        byte[] newBuffer;
        while (bufferCapacity < len) {
            if (bufferCapacity != 0) {
                bufferCapacity += Math.min(BUFFER_GROW_MAX_INCREMENT,
                        bufferCapacity * BUFFER_GROW_MULT);
            } else {
                bufferCapacity = BUFFER_START_CAPACITY;
            }
        }
        newBuffer = new byte[bufferCapacity];
        buffer = newBuffer;
    }

    private void appendBuffer(byte c) {
        if (bufferLen == bufferCapacity) {
            resizeBuffer();
        }
        buffer[bufferLen++] = c;
    }

    private String getBuffer() {
        String s;
        s = new String(buffer, 0, bufferLen);
        resetBuffer();
        return s;
    }


    private void throwOME(String message, Exception e) throws OMException {
        OMException ome =
                new OMException(message + " (at byte: " + pos + ")", e);
        if (parserErrorHandler != null) {
            parserErrorHandler.fatalError(ome);
        } else {
            throw ome;
        }
    }

    private void throwOME(String message) throws OMException {
        throwOME(message, (Exception) null);
    }

    private int parseLen(int bigLen) throws IOException {
        int b3, b2, b1, b0;
        b3 = source.readUnsignedByte();
        pos++;
        if (bigLen == 0) {
            //            System.out.println("parseLen: " + b3);
            return b3;
        } else {
            b2 = source.readUnsignedByte();
            b1 = source.readUnsignedByte();
            b0 = source.readUnsignedByte();
            //            System.out.println("parseLen: big " +
            //      ((b3 << 24) | (b2 << 16) | (b1 << 8) | b0));
            return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
        }
    }

    private Object parseShareId(Table table) throws IOException {
        short id;
        id = (short) getByte();
        return table.share[id];
    }


    private void parseSharedVar() throws IOException {
        String var;
        var = (String) parseShareId(variableTable);
        parserHandler.readVariable(var);
    }

    private void parseSharedString() throws IOException {
        String s;
        s = (String) parseShareId(stringTable);
        parserHandler.readString(s);
    }

    private void parseSharedWCString() throws IOException {
        String s;
        s = (String) parseShareId(stringTable);
        parserHandler.readString(s);
    }

    private void parseSharedSymbol() throws IOException {
        Symbol s;
        s = (Symbol) parseShareId(symbolTable);
        parserHandler.readSymbol(s);
    }

    private void parseInt32(int bigLen) throws IOException {
        int i;
        if (bigLen == 0) {
            parserHandler.readInteger((int) source.readByte());
            pos += 2;
        } else {
            parserHandler.readInteger(source.readInt());
            pos += 5;
        }
    }

    private void parseBigInteger(int bigLen) throws IOException {
        int l, bs;
        String digits;
        BigInteger bi;

        l = parseLen(bigLen);
        bs = getByte();
        digits = getString(l);
        switch (bs) {
            case Node.OM_BE_POS_BASE_16: 
                bi = new BigInteger(digits, 16);
                parserHandler.readInteger(bi);
                break;
            case Node.OM_BE_NEG_BASE_16: 
                bi = new BigInteger(digits, 16);
                parserHandler.readInteger(bi.negate());
                break;
            case Node.OM_BE_POS_BASE_10: 
                bi = new BigInteger(digits, 10);
                parserHandler.readInteger(bi);
                break;
            case Node.OM_BE_NEG_BASE_10: 
                bi = new BigInteger(digits, 10);
                parserHandler.readInteger(bi.negate());
                break;
            default: 
                throwOME("unknown int type. corrupted stream? found 'x" +
                        Integer.toString(bs, 16) + "')");
        }
    }

    private void parseFloat64() throws IOException {
        long l;

        l = source.readLong();
        pos += 8;
        l = (((l & 0xff00000000000000L) >>> 56) |
                ((l & 0x00ff000000000000L) >>> 40)
                | ((l & 0x0000ff0000000000L) >>> 24)
                | ((l & 0x000000ff00000000L) >>> 8)
                | ((l & 0x00000000ff000000L) << 8)
                | ((l & 0x0000000000ff0000L) << 24)
                | ((l & 0x000000000000ff00L) << 40)
                | ((l & 0x00000000000000ffL) << 56));
        parserHandler.readFloat(java.lang.Double.longBitsToDouble(l));
    }

    private void parseByteArray(int bigLen) throws IOException {
        byte raw[], ba[];
        int l;

        l = parseLen(bigLen);
        raw = getBytes(l);
        ba = new byte[l];
        System.arraycopy(raw, 0, ba, 0, l);
        parserHandler.readByteArray(ba);
    }

    private void parseVar(int bigLen) throws OMException, IOException {
        String s;
        int l;

        l = parseLen(bigLen);
        s = getString(l);
        variableTable.add(s);
        parserHandler.readVariable(s);
    }

    private void parseString(int bigLen) throws OMException, IOException {
        String s;
        int l;

        l = parseLen(bigLen);
        s = getString(l);
        stringTable.add(s);
        parserHandler.readString(s);
    }

    private void parseWCString(int bigLen) throws IOException {
        StringBuffer sb;
        String s;
        byte[] b;
        char c;
        int i, l, a, p = 0;

        l = parseLen(bigLen);
        b = getBytes(l);
        l = l >> 1;
        sb = new StringBuffer(l);
        for (i = 0; i < l; i++) {
            // System.out.print("(" + java.lang.Integer.toHexString(b[p])+" "+java.lang.Integer.toHexString(b[p+1]) + ")");
            c = (char) ((b[p] << 8) | ((b[p + 1]) & 0xff));
            p += 2;
            sb.append(c);
        }
        s = sb.toString();
        stringTable.add(s);
        parserHandler.readString(s);
    }

    private void parseSymbol(int bigLen) throws IOException {
        String cd, name;
        int cdLen, nameLen;
        Symbol s;

        cdLen = parseLen(bigLen);
        nameLen = parseLen(bigLen);
        cd = getString(cdLen);
        name = getString(nameLen);
        s = new Symbol(cd, name);
        symbolTable.add(s);
        parserHandler.readSymbol(s);
    }

    private void parseComment(int bigLen) throws IOException {
        String s;
        int l;

        l = parseLen(bigLen);
        s = getString(l);
        parserHandler.readComment(s);
    }

    private void parsePInstruction(int bigLen) throws IOException {
        String s;
        int l;

        l = parseLen(bigLen);
        s = getString(l);
        parserHandler.readPInstruction(s);
    }

    private void parseElementAux() throws OMException, IOException {
        byte r;
        int c, t;
        boolean bigLen;

        r = getByte();
        c = r & ~Node.OM_BE_BIG_LEN_MASK;
        if (0 != (c & Node.OM_BE_SHARED_MASK)) {
            // shared tag
            t = c & ~Node.OM_BE_SHARED_MASK;
            switch (t) {
                case Node.OM_VAR: 
                    parseSharedVar();
                    break;
                case Node.OM_STRING: 
                    parseSharedString();
                    break;
                case Node.OM_WCSTRING: 
                    parseSharedWCString();
                    break;
                case Node.OM_SYMBOL: 
                    parseSharedSymbol();
                    break;
                default: 
                    ungetByte(r);
                    throwOME("can't find a legal tag." +
                            " corrupted stream? (found 'x" + Integer.toString(
                            r, 16) + "' shared 'x" + Integer.toString(t, 16) +
                            "')");
            }
        } else {
            // unshared tag
            switch (c) {
                case Node.OM_INT32: 
                    parseInt32(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                case Node.OM_BIGINT: 
                    parseBigInteger(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                case Node.OM_FLOAT64: 
                    parseFloat64();
                    break;
                case Node.OM_BYTEARRAY: 
                    parseByteArray(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                case Node.OM_VAR: 
                    parseVar(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                case Node.OM_STRING: 
                    parseString(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                case Node.OM_WCSTRING: 
                    parseWCString(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                case Node.OM_SYMBOL: 
                    parseSymbol(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                case Node.OM_OBJECT: 
                    stringTable.clear();
                    symbolTable.clear();
                    variableTable.clear();
                    parserHandler.startObject();
                    break;
                case Node.OM_END_OBJECT: 
                    parserHandler.endObject();
                    break;
                case Node.OM_APP: 
                    parserHandler.startApplication();
                    break;
                case Node.OM_END_APP: 
                    parserHandler.endApplication();
                    break;
                case Node.OM_ATTR: 
                    parserHandler.startAttribution();
                    break;
                case Node.OM_END_ATTR: 
                    parserHandler.endAttribution();
                    break;
                case Node.OM_ATP: 
                    parserHandler.startAttributePairs();
                    break;
                case Node.OM_END_ATP: 
                    parserHandler.endAttributePairs();
                    break;
                case Node.OM_BVAR: 
                    parserHandler.startBVars();
                    break;
                case Node.OM_END_BVAR: 
                    parserHandler.endBVars();
                    break;
                case Node.OM_ERROR: 
                    parserHandler.startError();
                    break;
                case Node.OM_END_ERROR: 
                    parserHandler.endError();
                    break;
                case Node.OM_BIND: 
                    parserHandler.startBind();
                    break;
                case Node.OM_END_BIND: 
                    parserHandler.endBind();
                    break;
                case Node.OM_COMMENT: 
                    parseComment(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                case Node.OM_PINSTRUCTION: 
                    parsePInstruction(r & Node.OM_BE_BIG_LEN_MASK);
                    break;
                default: 
                    ungetByte(r);
                    throwOME("can't find a legal tag." +
                            " corrupted stream? (found 'x" + Integer.toString(
                            c, 16) + "')");
            }
        }
    }


    /**
     * @see fr.inria.openmath.omapi.Parser#initParse
     */
    public void initParse(InputStream source) throws OMException {
        initDone = true;
        pos = 0;
        if (!(source.getClass()).isAssignableFrom(
                (new DataInputStream(source)).getClass())) {
            throw new RuntimeException(
                    this.getClass().getName() +
                    "[parse]: invalid call, source must be a DataInputStream or assignable. (found "
                    + source.getClass().getName() + ")");
        }
        this.source = (DataInputStream) source;
        if (stringTable == null)
            stringTable = new Table();
        else
            stringTable.clear();
        if (symbolTable == null)
            symbolTable = new Table();
        else
            symbolTable.clear();
        if (variableTable == null)
            variableTable = new Table();
        else
            variableTable.clear();

        parserHandler.setLocator(this);
        parserHandler.startParse();
    }

    private void checkInit() {
        if (!initDone) {
            throwOME("initParse() has not been called!");
        }
    }

    /**
     * @see fr.inria.openmath.omapi.Parser#parseElement
     */
    public void parseElement(InputStream source) throws OMException {
        checkInit();
        try {
            try {
                parseElementAux();
            } catch (IOException e) {
                parserHandler.endParse();
            }
        } catch (OMException e) {
            if (e.getErrorCode() != OMException.OME_END_OBJECT)
                throw e;
        }
    }

    /**
     * @see fr.inria.openmath.omapi.Parser#parseObject
     */
    public void parseObject(InputStream source) throws OMException {
        checkInit();
        try {
            try {
                while (true) {
                    parseElementAux();
                }
            } catch (IOException e) {
                parserHandler.endParse();
            }
        } catch (OMException e) {
            if (e.getErrorCode() != OMException.OME_END_OBJECT)
                throw e;
        }
    }

    /**
     * @see fr.inria.openmath.omapi.Parser#parse
     */
    public void parse(InputStream source) throws OMException {
        checkInit();
        try {
            while (true) {
                try {
                    while (true) {
                        parseElementAux();
                    }
                } catch (OMException e) {
                    if (e.getErrorCode() != OMException.OME_END_OBJECT)
                        throw e;
                }
            }
        } catch (IOException e) {
            parserHandler.endParse();
        }
    }


    /**
     * @see fr.inria.openmath.omapi.Locator#getLineNumber
     */
    public int getLineNumber() {
        return 0;
    }

    /**
     * @see fr.inria.openmath.omapi.Locator#getColumnNumber
     */
    public int getColumnNumber() {
        return pos;
    }

    /**
     * @see fr.inria.openmath.omapi.Parser#setParserHandler
     */
    public void setParserHandler(ParserHandler handler) {
        this.parserHandler = handler;
    }

    /**
     * @see fr.inria.openmath.omapi.Parser#getParserHandler
     */
    public ParserHandler getParserHandler() {
        return this.parserHandler;
    }

    /**
     * @see fr.inria.openmath.omapi.Parser#setParserErrorHandler
     */
    public void setParserErrorHandler(ParserErrorHandler handler) {
        this.parserErrorHandler = handler;
    }

    /**
     * @see fr.inria.openmath.omapi.Parser#getParserErrorHandler
     */
    public ParserErrorHandler getParserErrorHandler() {
        return this.parserErrorHandler;
    }


}
